* The feed should be skipped if there isn't a Commerce Partner Integration ID set as the ID is required for * calls to the GraphCommercePartnerIntegrationFileUpdatePost endpoint. * Overwrite this function if your feed upload uses a different endpoint with different requirements. * * @since 3.5.0 */ public function should_skip_feed(): bool { $connection_handler = facebook_for_woocommerce()->get_connection_handler(); $cpi_id = $connection_handler->get_commerce_partner_integration_id(); $cms_id = $connection_handler->get_commerce_merchant_settings_id(); return empty( $cpi_id ) || empty( $cms_id ); } /** * Trigger the upload flow * Once feed regenerated, trigger upload via create_upload API * This will hit the url defined in the class and trigger handle_feed_data_request * * @since 3.5.0 */ public function send_request_to_upload_feed(): void { $name = static::get_data_stream_name(); $data = array( 'url' => self::get_feed_data_url(), 'feed_type' => static::get_feed_type(), 'update_type' => 'CREATE', ); try { $cpi_id = facebook_for_woocommerce()->get_connection_handler()->get_commerce_partner_integration_id(); facebook_for_woocommerce()-> get_api()-> create_common_data_feed_upload( $cpi_id, $data ); } catch ( \Exception $exception ) { Logger::log( 'Abstract feed upload failed.', array( 'event' => 'feed_upload', 'event_type' => 'send_request_to_upload_feed', 'extra_data' => [ 'feed_name' => $name, 'data' => wp_json_encode( $data ), ], ), array( 'should_send_log_to_meta' => true, 'should_save_log_in_woocommerce' => false, 'woocommerce_log_level' => \WC_Log_Levels::DEBUG, ), $exception, ); } } /** * Gets the URL for retrieving the feed data using legacy WooCommerce REST API. * Sample url: * https://your-site-url.com/?wc-api=wc_facebook_get_feed_data_example&secret=your_generated_secret * * @return string * @since 3.5.0 */ public function get_feed_data_url(): string { $query_args = array( 'wc-api' => self::REQUEST_FEED_ACTION . static::get_data_stream_name(), 'secret' => self::get_feed_secret(), ); // phpcs:ignore // nosemgrep: audit.php.wp.security.xss.query-arg return add_query_arg( $query_args, home_url( '/' ) ); } /** * Gets the secret value that should be included in the legacy WooCommerce REST API URL. * * @return string * @since 3.5.0 */ public function get_feed_secret(): string { $secret_option_name = self::OPTION_FEED_URL_SECRET . static::get_data_stream_name(); $secret = get_option( $secret_option_name, '' ); if ( ! $secret ) { $secret = wp_hash( 'example-feed-' . time() ); update_option( $secret_option_name, $secret ); } return $secret; } /** * Callback function that streams the feed file to the GraphPartnerIntegrationFileUpdatePost * Ex: https://your-site-url.com/?wc-api=wc_facebook_get_feed_data_example&secret=your_generated_secret * The above WooC Legacy REST API will trigger the handle_feed_data_request method * See LegacyRequestApiStub.php for more details * * @throws PluginException If file issue comes up. * @since 3.5.0 */ public function handle_feed_data_request(): void { $name = static::get_data_stream_name(); Logger::log( "{$name} feed: Meta is requesting feed file.", [], array( 'should_send_log_to_meta' => false, 'should_save_log_in_woocommerce' => true, 'woocommerce_log_level' => \WC_Log_Levels::DEBUG, ) ); $file_path = $this->feed_writer->get_file_path(); $file = false; // regenerate if the file doesn't exist using the legacy flow. if ( ! file_exists( $file_path ) ) { $this->feed_handler->generate_feed_file(); } try { // bail early if the feed secret is not included or is not valid. if ( self::get_feed_secret() !== Helper::get_requested_value( 'secret' ) ) { throw new PluginException( "{$name} feed: Invalid secret provided.", 401 ); } // bail early if the file can't be read. if ( ! is_readable( $file_path ) ) { throw new PluginException( "{$name}: File at path ' . $file_path . ' is not readable.", 404 ); } if ( $this->feed_writer instanceof JsonFeedFileWriter ) { $content_type = 'Content-Type: application/json; charset=utf-8'; } else { $content_type = 'Content-Type: text/csv; charset=utf-8'; } // set the download headers. header( $content_type ); header( 'Content-Description: File Transfer' ); header( 'Content-Disposition: attachment; filename="' . basename( $file_path ) . '"' ); header( 'Expires: 0' ); header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' ); header( 'Pragma: public' ); header( 'Content-Length:' . filesize( $file_path ) ); // phpcs:ignore -- use php file i/o functions $file = fopen( $file_path, 'rb' ); if ( ! $file ) { throw new PluginException( "{$name} feed: Could not open feed file.", 500 ); } // fpassthru might be disabled in some hosts (like Flywheel). // phpcs:ignore if ( \WC_Facebookcommerce_Utils::is_fpassthru_disabled() || ! @fpassthru( $file ) ) { Logger::log( "{$name} feed: fpassthru is disabled: getting file contents.", [], array( 'should_send_log_to_meta' => false, 'should_save_log_in_woocommerce' => true, 'woocommerce_log_level' => \WC_Log_Levels::DEBUG, ) ); //phpcs:ignore $contents = @stream_get_contents( $file ); if ( ! $contents ) { throw new PluginException( "{$name} feed: Could not get feed file contents.", 500 ); } echo $contents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } catch ( \Exception $exception ) { Logger::log( 'Error while handling Feed data request.', array( 'event' => 'feed_upload', 'event_type' => 'handle_feed_data_request', 'extra_data' => [ 'feed_name' => $name, 'file_path' => $file_path, ], ), array( 'should_send_log_to_meta' => true, 'should_save_log_in_woocommerce' => false, 'woocommerce_log_level' => \WC_Log_Levels::DEBUG, ), $exception, ); status_header( $exception->getCode() ); } finally { if ( $file ) { // phpcs:ignore -- use php file i/o functions fclose($file); } } exit; } /** * Get the data stream name for the given feed. * * @return string */ abstract protected static function get_data_stream_name(): string; /** * Get the data feed type. * * @return string */ abstract protected static function get_feed_type(): string; /** * Get the feed generation interval. Must be longer than the heartbeat. * * @return int */ protected static function get_feed_gen_interval(): int { return DAY_IN_SECONDS; } /** * Get the Heartbeat interval to ensure that feed gen is scheduled. Must be shorter than the feed gen interval. * * @return string Heartbeat constant value */ protected static function get_feed_gen_scheduling_interval(): string { return Heartbeat::HOURLY; } }